#!/usr/bin/perl -w
#
#   Copyright (c) International Business Machines  Corp., 2002
#
#   This program is free software;  you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or (at
#   your option) any later version.
#
#   This program is distributed in the hope that it will be useful, but
#   WITHOUT ANY WARRANTY;  without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#   General Public License for more details.                 
#
#   You should have received a copy of the GNU General Public License
#   along with this program;  if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
# genpng
#
#   This script creates an overview PNG image of a source code file by
#   representing each source code character by a single pixel.
#
#   Note that the Perl module GD.pm is required for this script to work.
#   It may be obtained from http://www.cpan.org
#
# History:
#   2002-08-26: created by Peter Oberparleiter <Peter.Oberparleiter@de.ibm.com>
#

use strict;
use File::Basename; 
use Getopt::Long;


# Constants
our $lcov_version	= 'LCOV version 1.10';
our $lcov_url		= "http://ltp.sourceforge.net/coverage/lcov.php";
our $tool_name		= basename($0);


# Prototypes
sub gen_png($$$@);
sub check_and_load_module($);
sub genpng_print_usage(*);
sub genpng_process_file($$$$);
sub genpng_warn_handler($);
sub genpng_die_handler($);


#
# Code entry point
#

# Prettify version string
$lcov_version =~ s/\$\s*Revision\s*:?\s*(\S+)\s*\$/$1/;

# Check whether required module GD.pm is installed
if (check_and_load_module("GD"))
{
	# Note: cannot use die() to print this message because inserting this
	# code into another script via do() would not fail as required!
	print(STDERR <<END_OF_TEXT)
ERROR: required module GD.pm not found on this system (see www.cpan.org).
END_OF_TEXT
	;
	exit(2);
}

# Check whether we're called from the command line or from another script
if (!caller)
{
	my $filename;
	my $tab_size = 4;
	my $width = 80;
	my $out_filename;
	my $help;
	my $version;

	$SIG{__WARN__} = \&genpng_warn_handler;
	$SIG{__DIE__} = \&genpng_die_handler;

	# Parse command line options
	if (!GetOptions("tab-size=i" => \$tab_size,
			"width=i" => \$width,
			"output-filename=s" => \$out_filename,
			"help" => \$help,
			"version" => \$version))
	{
		print(STDERR "Use $tool_name --help to get usage ".
		      "information\n");
		exit(1);
	}

	$filename = $ARGV[0];

	# Check for help flag
	if ($help)
	{
		genpng_print_usage(*STDOUT);
		exit(0);
	}

	# Check for version flag
	if ($version)
	{
		print("$tool_name: $lcov_version\n");
		exit(0);
	}

	# Check options
	if (!$filename)
	{
		die("No filename specified\n");
	}

	# Check for output filename
	if (!$out_filename)
	{
		$out_filename = "$filename.png";
	}

	genpng_process_file($filename, $out_filename, $width, $tab_size);
	exit(0);
}


#
# genpng_print_usage(handle)
#
# Write out command line usage information to given filehandle.
#

sub genpng_print_usage(*)
{
	local *HANDLE = $_[0];

	print(HANDLE <<END_OF_USAGE)
Usage: $tool_name [OPTIONS] SOURCEFILE

Create an overview image for a given source code file of either plain text
or .gcov file format.

  -h, --help                        Print this help, then exit
  -v, --version                     Print version number, then exit
  -t, --tab-size TABSIZE            Use TABSIZE spaces in place of tab
  -w, --width WIDTH                 Set width of output image to WIDTH pixel
  -o, --output-filename FILENAME    Write image to FILENAME

For more information see: $lcov_url
END_OF_USAGE
	;
}


#
# check_and_load_module(module_name)
#
# Check whether a module by the given name is installed on this system
# and make it known to the interpreter if available. Return undefined if it
# is installed, an error message otherwise.
#

sub check_and_load_module($)
{
	eval("use $_[0];");
	return $@;
}


#
# genpng_process_file(filename, out_filename, width, tab_size)
#

sub genpng_process_file($$$$)
{
	my $filename		= $_[0];
	my $out_filename	= $_[1];
	my $width		= $_[2];
	my $tab_size		= $_[3];
	local *HANDLE;
	my @source;

	open(HANDLE, "<", $filename)
		or die("ERROR: cannot open $filename!\n");

	# Check for .gcov filename extension
	if ($filename =~ /^(.*).gcov$/)
	{
		# Assume gcov text format
		while (<HANDLE>)
		{
			if (/^\t\t(.*)$/)
			{
				# Uninstrumented line
				push(@source, ":$1");
			}
			elsif (/^      ######    (.*)$/)
			{
				# Line with zero execution count
				push(@source, "0:$1");
			}
			elsif (/^( *)(\d*)    (.*)$/)
			{
				# Line with positive execution count
				push(@source, "$2:$3");
			}
		}
	}
	else
	{
		# Plain text file
		while (<HANDLE>) { push(@source, ":$_"); }
	}
	close(HANDLE);

	gen_png($out_filename, $width, $tab_size, @source);
}


#
# gen_png(filename, width, tab_size, source)
#
# Write an overview PNG file to FILENAME. Source code is defined by SOURCE
# which is a list of lines <count>:<source code> per source code line.
# The output image will be made up of one pixel per character of source,
# coloring will be done according to execution counts. WIDTH defines the
# image width. TAB_SIZE specifies the number of spaces to use as replacement
# string for tabulator signs in source code text.
#
# Die on error.
#

sub gen_png($$$@)
{
	my $filename = shift(@_);	# Filename for PNG file
	my $overview_width = shift(@_);	# Imagewidth for image
	my $tab_size = shift(@_);	# Replacement string for tab signs
	my @source = @_;	# Source code as passed via argument 2
	my $height;		# Height as define by source size
	my $overview;		# Source code overview image data
	my $col_plain_back;	# Color for overview background
	my $col_plain_text;	# Color for uninstrumented text
	my $col_cov_back;	# Color for background of covered lines
	my $col_cov_text;	# Color for text of covered lines
	my $col_nocov_back;	# Color for background of lines which
				# were not covered (count == 0)
	my $col_nocov_text;	# Color for test of lines which were not
				# covered (count == 0)
	my $col_hi_back;	# Color for background of highlighted lines
	my $col_hi_text;	# Color for text of highlighted lines
	my $line;		# Current line during iteration
	my $row = 0;		# Current row number during iteration
	my $column;		# Current column number during iteration
	my $color_text;		# Current text color during iteration
	my $color_back;		# Current background color during iteration
	my $last_count;		# Count of last processed line
	my $count;		# Count of current line
	my $source;		# Source code of current line
	my $replacement;	# Replacement string for tabulator chars
	local *PNG_HANDLE;	# Handle for output PNG file

	# Handle empty source files
	if (!@source) {
		@source = ( "" );
	}
	$height = scalar(@source);
	# Create image
	$overview = new GD::Image($overview_width, $height)
		or die("ERROR: cannot allocate overview image!\n");

	# Define colors
	$col_plain_back	= $overview->colorAllocate(0xff, 0xff, 0xff);
	$col_plain_text	= $overview->colorAllocate(0xaa, 0xaa, 0xaa);
	$col_cov_back	= $overview->colorAllocate(0xaa, 0xa7, 0xef);
	$col_cov_text	= $overview->colorAllocate(0x5d, 0x5d, 0xea);
	$col_nocov_back = $overview->colorAllocate(0xff, 0x00, 0x00);
	$col_nocov_text = $overview->colorAllocate(0xaa, 0x00, 0x00);
	$col_hi_back = $overview->colorAllocate(0x00, 0xff, 0x00);
	$col_hi_text = $overview->colorAllocate(0x00, 0xaa, 0x00);

	# Visualize each line
	foreach $line (@source)
	{
		# Replace tabs with spaces to keep consistent with source
		# code view
		while ($line =~ /^([^\t]*)(\t)/)
		{
			$replacement = " "x($tab_size - ((length($1) - 1) %
				       $tab_size));
			$line =~ s/^([^\t]*)(\t)/$1$replacement/;
		}

		# Skip lines which do not follow the <count>:<line>
		# specification, otherwise $1 = count, $2 = source code
		if (!($line =~ /(\*?)(\d*):(.*)$/)) { next; }
		$count = $2;
		$source = $3;

		# Decide which color pair to use

		# If this line was not instrumented but the one before was,
		# take the color of that line to widen color areas in
		# resulting image
		if (($count eq "") && defined($last_count) &&
		    ($last_count ne ""))
		{
			$count = $last_count;
		}

		if ($count eq "")
		{
			# Line was not instrumented
			$color_text = $col_plain_text;
			$color_back = $col_plain_back;
		}
		elsif ($count == 0)
		{
			# Line was instrumented but not executed
			$color_text = $col_nocov_text;
			$color_back = $col_nocov_back;
		}
		elsif ($1 eq "*")
		{
			# Line was highlighted
			$color_text = $col_hi_text;
			$color_back = $col_hi_back;
		}
		else
		{
			# Line was instrumented and executed
			$color_text = $col_cov_text;
			$color_back = $col_cov_back;
		}

		# Write one pixel for each source character
		$column = 0;
		foreach (split("", $source))
		{
			# Check for width
			if ($column >= $overview_width) { last; }

			if ($_ eq " ")
			{
				# Space
				$overview->setPixel($column++, $row,
						    $color_back);
			}
			else
			{
				# Text
				$overview->setPixel($column++, $row,
						    $color_text);
			}
		}

		# Fill rest of line		
		while ($column < $overview_width)
		{
			$overview->setPixel($column++, $row, $color_back);
		}

		$last_count = $2;

		$row++;
	}

	# Write PNG file
	open (PNG_HANDLE, ">", $filename)
		or die("ERROR: cannot write png file $filename!\n");
	binmode(*PNG_HANDLE);
	print(PNG_HANDLE $overview->png());
	close(PNG_HANDLE);
}

sub genpng_warn_handler($)
{
	my ($msg) = @_;

	warn("$tool_name: $msg");
}

sub genpng_die_handler($)
{
	my ($msg) = @_;

	die("$tool_name: $msg");
}
